/*
 * Serial back end (Unix-specific).
 */

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <limits.h>

#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>

#include "putty.h"
#include "tree234.h"

#define SERIAL_MAX_BACKLOG 4096

typedef struct serial_backend_data {
  void *frontend;
  int fd;
  int finished;
  int inbufsize;
  bufchain output_data;
} * Serial;

/*
 * We store our serial backends in a tree sorted by fd, so that
 * when we get an uxsel notification we know which backend instance
 * is the owner of the serial port that caused it.
 */
static int serial_compare_by_fd(void *av, void *bv)
{
  Serial a = (Serial)av;
  Serial b = (Serial)bv;

  if (a->fd < b->fd)
    return -1;
  else if (a->fd > b->fd)
    return +1;
  return 0;
}

static int serial_find_by_fd(void *av, void *bv)
{
  int a = *(int *)av;
  Serial b = (Serial)bv;

  if (a < b->fd)
    return -1;
  else if (a > b->fd)
    return +1;
  return 0;
}

static tree234 *serial_by_fd = NULL;

static void serial_select_result(int fd, int event);
static void serial_uxsel_setup(Serial serial);
static void serial_try_write(Serial serial);

static const char *serial_configure(Serial serial, Conf *conf)
{
  struct termios options;
  int bflag, bval, speed, flow, parity;
  const char *str;
  char *msg;

  if (serial->fd < 0)
    return "Unable to reconfigure already-closed serial connection";

  tcgetattr(serial->fd, &options);

  /*
   * Find the appropriate baud rate flag.
   */
  speed = conf_get_int(conf, CONF_serspeed);
#define SETBAUD(x) (bflag = B##x, bval = x)
#define CHECKBAUD(x)                                                           \
  do {                                                                         \
    if (speed >= x)                                                            \
      SETBAUD(x);                                                              \
  } while (0)
  SETBAUD(50);
#ifdef B75
  CHECKBAUD(75);
#endif
#ifdef B110
  CHECKBAUD(110);
#endif
#ifdef B134
  CHECKBAUD(134);
#endif
#ifdef B150
  CHECKBAUD(150);
#endif
#ifdef B200
  CHECKBAUD(200);
#endif
#ifdef B300
  CHECKBAUD(300);
#endif
#ifdef B600
  CHECKBAUD(600);
#endif
#ifdef B1200
  CHECKBAUD(1200);
#endif
#ifdef B1800
  CHECKBAUD(1800);
#endif
#ifdef B2400
  CHECKBAUD(2400);
#endif
#ifdef B4800
  CHECKBAUD(4800);
#endif
#ifdef B9600
  CHECKBAUD(9600);
#endif
#ifdef B19200
  CHECKBAUD(19200);
#endif
#ifdef B38400
  CHECKBAUD(38400);
#endif
#ifdef B57600
  CHECKBAUD(57600);
#endif
#ifdef B76800
  CHECKBAUD(76800);
#endif
#ifdef B115200
  CHECKBAUD(115200);
#endif
#ifdef B153600
  CHECKBAUD(153600);
#endif
#ifdef B230400
  CHECKBAUD(230400);
#endif
#ifdef B307200
  CHECKBAUD(307200);
#endif
#ifdef B460800
  CHECKBAUD(460800);
#endif
#ifdef B500000
  CHECKBAUD(500000);
#endif
#ifdef B576000
  CHECKBAUD(576000);
#endif
#ifdef B921600
  CHECKBAUD(921600);
#endif
#ifdef B1000000
  CHECKBAUD(1000000);
#endif
#ifdef B1152000
  CHECKBAUD(1152000);
#endif
#ifdef B1500000
  CHECKBAUD(1500000);
#endif
#ifdef B2000000
  CHECKBAUD(2000000);
#endif
#ifdef B2500000
  CHECKBAUD(2500000);
#endif
#ifdef B3000000
  CHECKBAUD(3000000);
#endif
#ifdef B3500000
  CHECKBAUD(3500000);
#endif
#ifdef B4000000
  CHECKBAUD(4000000);
#endif
#undef CHECKBAUD
#undef SETBAUD
  cfsetispeed(&options, bflag);
  cfsetospeed(&options, bflag);
  msg = dupprintf("Configuring baud rate %d", bval);
  logevent(serial->frontend, msg);
  sfree(msg);

  options.c_cflag &= ~CSIZE;
  switch (conf_get_int(conf, CONF_serdatabits)) {
  case 5:
    options.c_cflag |= CS5;
    break;
  case 6:
    options.c_cflag |= CS6;
    break;
  case 7:
    options.c_cflag |= CS7;
    break;
  case 8:
    options.c_cflag |= CS8;
    break;
  default:
    return "Invalid number of data bits (need 5, 6, 7 or 8)";
  }
  msg = dupprintf("Configuring %d data bits",
                  conf_get_int(conf, CONF_serdatabits));
  logevent(serial->frontend, msg);
  sfree(msg);

  if (conf_get_int(conf, CONF_serstopbits) >= 4) {
    options.c_cflag |= CSTOPB;
  } else {
    options.c_cflag &= ~CSTOPB;
  }
  msg =
      dupprintf("Configuring %d stop bits", (options.c_cflag & CSTOPB ? 2 : 1));
  logevent(serial->frontend, msg);
  sfree(msg);

  options.c_iflag &= ~(IXON | IXOFF);
#ifdef CRTSCTS
  options.c_cflag &= ~CRTSCTS;
#endif
#ifdef CNEW_RTSCTS
  options.c_cflag &= ~CNEW_RTSCTS;
#endif
  flow = conf_get_int(conf, CONF_serflow);
  if (flow == SER_FLOW_XONXOFF) {
    options.c_iflag |= IXON | IXOFF;
    str = "XON/XOFF";
  } else if (flow == SER_FLOW_RTSCTS) {
#ifdef CRTSCTS
    options.c_cflag |= CRTSCTS;
#endif
#ifdef CNEW_RTSCTS
    options.c_cflag |= CNEW_RTSCTS;
#endif
    str = "RTS/CTS";
  } else
    str = "no";
  msg = dupprintf("Configuring %s flow control", str);
  logevent(serial->frontend, msg);
  sfree(msg);

  /* Parity */
  parity = conf_get_int(conf, CONF_serparity);
  if (parity == SER_PAR_ODD) {
    options.c_cflag |= PARENB;
    options.c_cflag |= PARODD;
    str = "odd";
  } else if (parity == SER_PAR_EVEN) {
    options.c_cflag |= PARENB;
    options.c_cflag &= ~PARODD;
    str = "even";
  } else {
    options.c_cflag &= ~PARENB;
    str = "no";
  }
  msg = dupprintf("Configuring %s parity", str);
  logevent(serial->frontend, msg);
  sfree(msg);

  options.c_cflag |= CLOCAL | CREAD;
  options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
  options.c_iflag &= ~(ISTRIP | IGNCR | INLCR | ICRNL
#ifdef IUCLC
                       | IUCLC
#endif
  );
  options.c_oflag &= ~(OPOST
#ifdef ONLCR
                       | ONLCR
#endif
#ifdef OCRNL
                       | OCRNL
#endif
#ifdef ONOCR
                       | ONOCR
#endif
#ifdef ONLRET
                       | ONLRET
#endif
  );
  options.c_cc[VMIN] = 1;
  options.c_cc[VTIME] = 0;

  if (tcsetattr(serial->fd, TCSANOW, &options) < 0)
    return "Unable to configure serial port";

  return NULL;
}

/*
 * Called to set up the serial connection.
 *
 * Returns an error message, or NULL on success.
 *
 * Also places the canonical host name into `realhost'. It must be
 * freed by the caller.
 */
static const char *serial_init(void *frontend_handle,
                               void **backend_handle,
                               Conf *conf,
                               const char *host,
                               int port,
                               char **realhost,
                               int nodelay,
                               int keepalive)
{
  Serial serial;
  const char *err;
  char *line;

  serial = snew(struct serial_backend_data);
  *backend_handle = serial;

  serial->frontend = frontend_handle;
  serial->finished = FALSE;
  serial->inbufsize = 0;
  bufchain_init(&serial->output_data);

  line = conf_get_str(conf, CONF_serline);
  {
    char *msg = dupprintf("Opening serial device %s", line);
    logevent(serial->frontend, msg);
    sfree(msg);
  }

  serial->fd = open(line, O_RDWR | O_NOCTTY | O_NDELAY | O_NONBLOCK);
  if (serial->fd < 0)
    return "Unable to open serial port";

  cloexec(serial->fd);

  err = serial_configure(serial, conf);
  if (err)
    return err;

  *realhost = dupstr(line);

  if (!serial_by_fd)
    serial_by_fd = newtree234(serial_compare_by_fd);
  add234(serial_by_fd, serial);

  serial_uxsel_setup(serial);

  /*
   * Specials are always available.
   */
  update_specials_menu(serial->frontend);

  return NULL;
}

static void serial_close(Serial serial)
{
  if (serial->fd >= 0) {
    close(serial->fd);
    serial->fd = -1;
  }
}

static void serial_free(void *handle)
{
  Serial serial = (Serial)handle;

  serial_close(serial);

  bufchain_clear(&serial->output_data);

  sfree(serial);
}

static void serial_reconfig(void *handle, Conf *conf)
{
  Serial serial = (Serial)handle;

  /*
   * FIXME: what should we do if this returns an error?
   */
  serial_configure(serial, conf);
}

static void serial_select_result(int fd, int event)
{
  Serial serial;
  char buf[4096];
  int ret;
  int finished = FALSE;

  serial = find234(serial_by_fd, &fd, serial_find_by_fd);

  if (!serial)
    return; /* spurious event; keep going */

  if (event == 1) {
    ret = read(serial->fd, buf, sizeof(buf));

    if (ret == 0) {
      /*
       * Shouldn't happen on a real serial port, but I'm open
       * to the idea that there might be two-way devices we
       * can treat _like_ serial ports which can return EOF.
       */
      finished = TRUE;
    } else if (ret < 0) {
#ifdef EAGAIN
      if (errno == EAGAIN)
        return; /* spurious */
#endif
#ifdef EWOULDBLOCK
      if (errno == EWOULDBLOCK)
        return; /* spurious */
#endif
      perror("read serial port");
      exit(1);
    } else if (ret > 0) {
      serial->inbufsize = from_backend(serial->frontend, 0, buf, ret);
      serial_uxsel_setup(serial); /* might acquire backlog and freeze */
    }
  } else if (event == 2) {
    /*
     * Attempt to send data down the pty.
     */
    serial_try_write(serial);
  }

  if (finished) {
    serial_close(serial);

    serial->finished = TRUE;

    notify_remote_exit(serial->frontend);
  }
}

static void serial_uxsel_setup(Serial serial)
{
  int rwx = 0;

  if (serial->inbufsize <= SERIAL_MAX_BACKLOG)
    rwx |= 1;
  if (bufchain_size(&serial->output_data))
    rwx |= 2; /* might also want to write to it */
  uxsel_set(serial->fd, rwx, serial_select_result);
}

static void serial_try_write(Serial serial)
{
  void *data;
  int len, ret;

  assert(serial->fd >= 0);

  while (bufchain_size(&serial->output_data) > 0) {
    bufchain_prefix(&serial->output_data, &data, &len);
    ret = write(serial->fd, data, len);

    if (ret < 0 && (errno == EWOULDBLOCK)) {
      /*
       * We've sent all we can for the moment.
       */
      break;
    }
    if (ret < 0) {
      perror("write serial port");
      exit(1);
    }
    bufchain_consume(&serial->output_data, ret);
  }

  serial_uxsel_setup(serial);
}

/*
 * Called to send data down the serial connection.
 */
static int serial_send(void *handle, const char *buf, int len)
{
  Serial serial = (Serial)handle;

  if (serial->fd < 0)
    return 0;

  bufchain_add(&serial->output_data, buf, len);
  serial_try_write(serial);

  return bufchain_size(&serial->output_data);
}

/*
 * Called to query the current sendability status.
 */
static int serial_sendbuffer(void *handle)
{
  Serial serial = (Serial)handle;
  return bufchain_size(&serial->output_data);
}

/*
 * Called to set the size of the window
 */
static void serial_size(void *handle, int width, int height)
{
  /* Do nothing! */
  return;
}

/*
 * Send serial special codes.
 */
static void serial_special(void *handle, Telnet_Special code)
{
  Serial serial = (Serial)handle;

  if (serial->fd >= 0 && code == TS_BRK) {
    tcsendbreak(serial->fd, 0);
    logevent(serial->frontend, "Sending serial break at user request");
  }

  return;
}

/*
 * Return a list of the special codes that make sense in this
 * protocol.
 */
static const struct telnet_special *serial_get_specials(void *handle)
{
  static const struct telnet_special specials[] = {{"Break", TS_BRK},
                                                   {NULL, TS_EXITMENU}};
  return specials;
}

static int serial_connected(void *handle)
{
  return 1; /* always connected */
}

static int serial_sendok(void *handle)
{
  return 1;
}

static void serial_unthrottle(void *handle, int backlog)
{
  Serial serial = (Serial)handle;
  serial->inbufsize = backlog;
  serial_uxsel_setup(serial);
}

static int serial_ldisc(void *handle, int option)
{
  /*
   * Local editing and local echo are off by default.
   */
  return 0;
}

static void serial_provide_ldisc(void *handle, void *ldisc)
{
  /* This is a stub. */
}

static void serial_provide_logctx(void *handle, void *logctx)
{
  /* This is a stub. */
}

static int serial_exitcode(void *handle)
{
  Serial serial = (Serial)handle;
  if (serial->fd >= 0)
    return -1; /* still connected */
  else
    /* Exit codes are a meaningless concept with serial ports */
    return INT_MAX;
}

/*
 * cfg_info for Serial does nothing at all.
 */
static int serial_cfg_info(void *handle)
{
  return 0;
}

Backend serial_backend = {serial_init,
                          serial_free,
                          serial_reconfig,
                          serial_send,
                          serial_sendbuffer,
                          serial_size,
                          serial_special,
                          serial_get_specials,
                          serial_connected,
                          serial_exitcode,
                          serial_sendok,
                          serial_ldisc,
                          serial_provide_ldisc,
                          serial_provide_logctx,
                          serial_unthrottle,
                          serial_cfg_info,
                          NULL /* test_for_upstream */,
                          "serial",
                          PROT_SERIAL,
                          0};
